.NET 与树莓派:TM1638 模块的按键扫描
↓推荐关注↓
前言
上一篇中老周马马虎虎地介绍 TM1638 的数码管驱动,这个模块除了驱动 LED 数码管,还有一个功能:按键扫描。记得前面的水文中老周写过一个 16 个按键的模块。那个是我们自己写代码去完成键扫描的。
但是,缺点是很明显的,它会占用我们应用的许多运行时间,尤其是在微控制器开发板上,资源就更紧张了。所以,有一个专门的芯片来做这些事情,可以大大地降低代码的执行时间开销。
读取 TM1638 模块的按键数据,其过程是这样的:
1、把STB线拉低;
2、发送读取按键的命令,一个字节;
3、DIO转为输入模式,读出四个字节。这四个字节包含按键信息;
4、拉高STB的电平。
时序如下图所示。
其中,Command1 就是读键命令,即 0100 0010。
internal enum TM1638Command : byte
{
// 读按钮扫描
ReadKeyScanData = 0b_0100_0010,
// 自动增加地址
AutoIncreaseAddress = 0b_0100_0000,
// 固定地址
FixAddress = 0b_0100_0100,
// 选择要读写的寄存器地址
SetDisplayAddress = 0b_1100_0000,
// 显示控制设置
DisplayControl = 0b_1000_0000
}
上回咱们已经写了 WriteByte 方法,现在,为了读按键数据,还要实现一个 ReadByte 方法。
byte ReadByte()
{
// 切换为输入模式
_gpio.SetPinMode(DIOPin, PinMode.Input);
// 从低位读起
byte tmp = 0;
for (int i = 0; i < 8; i++)
{
// 右移一位
tmp >>= 1;
// 拉低clk线
_gpio.Write(CLKPin, 0);
// 读电平
if ((bool)_gpio.Read(DIOPin))
{
tmp |= 0x80;
}
// 拉高clk线
_gpio.Write(CLKPin, 1);
}
// 还原为输出模式
_gpio.SetPinMode(DIOPin, PinMode.Output);
return tmp;
}
由于 TM1638 的大部分操作都是输出,只有读按键是输入操作,因此,在ReadByte方法中,先将 DIO 引脚改为输入模式,读完后改回输出模式。不过呢,因为这个模块只有这个命令是要读数据,其他命令都是写数据,而且这按键信息是一次性读四个字节,要是每读一个字节都切换一次输入输出,有点浪费性能,咱们把上面的代码去掉切换输入输出的代码。
byte ReadByte()
{
// 从低位读起
byte tmp = 0;
for (int i = 0; i < 8; i++)
{
……
// 拉高clk线
_gpio.Write(CLKPin, 1);
}
return tmp;
}
然后把输入输出切换的代码移到 ReadKey 方法中。
public int ReadKey()
{
// 拉低STB
_gpio.Write(STBPin, 0);
// 发送读按键命令
WriteByte((byte)TM1638Command.ReadKeyScanData);
// 切换为输入模式
_gpio.SetPinMode(DIOPin, PinMode.Input);
// 读四个字节
var keydata = new byte[4];
for(int i = 0; i < 4; i++)
{
keydata[i] = ReadByte();
}
// 拉高STB
_gpio.Write(STBPin, 1);
// 还原为输出模式
_gpio.SetPinMode(DIOPin, PinMode.Output);
// 分析按键
int keycode = -1;
if(keydata[0] == 0x01)
keycode = 0; // 按键1
else if(keydata[1] == 0x01)
keycode = 1; // 按键2
else if(keydata[2] == 0x01)
keycode = 2; // 按键3
else if(keydata[3] == 0x01)
keycode = 3; // 按键4
else if(keydata[0] == 0x10)
keycode = 4; // 按键5
else if(keydata[1] == 0x10)
keycode = 5; // 按键6
else if(keydata[2] == 0x10)
keycode = 6; // 按键7
else if(keydata[3] == 0x10)
keycode = 7; // 按键8
return keycode;
}
下面重点看看如何分析读到的这四个字。数据手册上有一个表。
总共有四个字节,每个字节有八位,因此,它能包含 24 个按键的信息,原理图如下:
K1、K2、K3 三根线,每根线并联出八个按键(KS1 - KS8),这就是它读扫描 24 键的原因。但,如果你买到的模块和老周一样,是八个按钮的,那就是只接通了 K3。然后我们把 K3 代入前面那个表格。
然而,模块的实际电路和数据手册上所标注的不一样,经老周测试,买到的这个模块的按键顺序是这样的。
if(keydata[0] == 0x01)
keycode = 0; // 按键1
else if(keydata[1] == 0x01)
keycode = 1; // 按键2
else if(keydata[2] == 0x01)
keycode = 2; // 按键3
else if(keydata[3] == 0x01)
keycode = 3; // 按键4
else if(keydata[0] == 0x10)
keycode = 4; // 按键5
else if(keydata[1] == 0x10)
keycode = 5; // 按键6
else if(keydata[2] == 0x10)
keycode = 6; // 按键7
else if(keydata[3] == 0x10)
keycode = 7; // 按键8
所以,你买回来的模块要亲自测一下,看看它在生产封装时是如何走线的。可以在读到字节后 WriteLine 输出一下,然后各个键按一遍,看看哪个对哪个。有可能不同厂子出来的模块接线顺序不同。
好了,现在 TM1638 类就完整了,老周重新上一遍代码。
using System;
using System.Device.Gpio;
namespace Devices
{
public class TM1638 : IDisposable
{
GpioController _gpio;
// 构造函数
public TM1638(int stbPin, int clkPin, int dioPin)
{
STBPin = stbPin; // STB 线连接的GPIO号
CLKPin = clkPin; // CLK 线连接的GPIO号
DIOPin = dioPin; // DIO 线连接的GPIO号
_gpio = new();
// 将各GPIO引脚初始化为输出模式
InitPins();
// 设置为固定地址模式
InitDisplay(true);
}
// 打开接口,设定为输出
private void InitPins()
{
_gpio.OpenPin(STBPin, PinMode.Output);
_gpio.OpenPin(CLKPin, PinMode.Output);
_gpio.OpenPin(DIOPin, PinMode.Output);
}
private void InitDisplay(bool isFix = true)
{
if (isFix)
{
WriteCommand((byte)TM1638Command.FixAddress);
}
else
{
WriteCommand((byte)TM1638Command.AutoIncreaseAddress);
}
// 清空显示
CleanChars();
CleanLEDs();
WriteCommand(0b1000_1111);
}
#region 公共属性
// 控制引脚号
public int STBPin { get; set; }
public int CLKPin { get; set; }
public int DIOPin { get; set; }
#endregion
public void Dispose()
{
_gpio?.Dispose();
}
#region 辅助方法
void WriteByte(byte val)
{
// 从低位传起
int i;
for (i = 0; i < 8; i++)
{
// 拉低clk线
_gpio.Write(CLKPin, 0);
// 修改dio线
if ((val & 0x01) == 0x01)
{
_gpio.Write(DIOPin, 1);
}
else
{
_gpio.Write(DIOPin, 0);
}
// 右移一位
val >>= 1;
//_gpio.Write(CLKPin, 0);
// 拉高clk线,向模块发出一位
_gpio.Write(CLKPin, 1);
}
}
// 读一个字节
byte ReadByte()
{
// 从低位读起
byte tmp = 0;
for (int i = 0; i < 8; i++)
{
// 右移一位
tmp >>= 1;
// 拉低clk线
_gpio.Write(CLKPin, 0);
// 读电平
if ((bool)_gpio.Read(DIOPin))
{
tmp |= 0x80;
}
// 拉高clk线
_gpio.Write(CLKPin, 1);
}
return tmp;
}
void WriteCommand(byte cmd, params byte[] data)
{
// 拉低stb
_gpio.Write(STBPin, 0);
WriteByte(cmd);
if (data.Length > 0)
{
// 写附加数据
foreach (byte b in data)
{
WriteByte(b);
}
}
// 拉高stb
_gpio.Write(STBPin, 1);
}
#endregion
public void SetChar(byte c, byte pos)
{
// 寄存器地址
byte reg = (byte)(pos * 2);
byte com = (byte)((byte)TM1638Command.SetDisplayAddress | reg);
WriteCommand(com, c);
}
public void SetLED(byte n, bool on)
{
byte addr = (byte)(n * 2 + 1); //寄存器地址
// 1100_xxxx
byte cmd = (byte)((byte)TM1638Command.SetDisplayAddress| addr );
byte data = (byte)(on? 1 : 0);
WriteCommand(cmd,data);
}
public void CleanChars()
{
int i = 0;
while(i < 8)
{
SetChar(0x00, (byte)i);
i++;
}
}
public void CleanLEDs()
{
int i=0;
while(i<8)
{
SetLED((byte)i, false);
i++;
}
}
public int ReadKey()
{
// 拉低STB
_gpio.Write(STBPin, 0);
// 发送读按键命令
WriteByte((byte)TM1638Command.ReadKeyScanData);
// 切换为输入模式
_gpio.SetPinMode(DIOPin, PinMode.Input);
// 读四个字节
var keydata = new byte[4];
for(int i = 0; i < 4; i++)
{
keydata[i] = ReadByte();
}
// 拉高STB
_gpio.Write(STBPin, 1);
// 还原为输出模式
_gpio.SetPinMode(DIOPin, PinMode.Output);
// 分析按键
int keycode = -1;
if(keydata[0] == 0x01)
keycode = 0; // 按键1
else if(keydata[1] == 0x01)
keycode = 1; // 按键2
else if(keydata[2] == 0x01)
keycode = 2; // 按键3
else if(keydata[3] == 0x01)
keycode = 3; // 按键4
else if(keydata[0] == 0x10)
keycode = 4; // 按键5
else if(keydata[1] == 0x10)
keycode = 5; // 按键6
else if(keydata[2] == 0x10)
keycode = 6; // 按键7
else if(keydata[3] == 0x10)
keycode = 7; // 按键8
return keycode;
}
}
internal enum TM1638Command : byte
{
// 读按钮扫描
ReadKeyScanData = 0b_0100_0010,
// 自动增加地址
AutoIncreaseAddress = 0b_0100_0000,
// 固定地址
FixAddress = 0b_0100_0100,
// 选择要读写的寄存器地址
SetDisplayAddress = 0b_1100_0000,
// 显示控制设置
DisplayControl = 0b_1000_0000
}
public class Numbers
{
public const byte Num0 = 0b_0011_1111; //0
public const byte Num1 = 0b_0000_0110; //1
public const byte Num2 = 0b_0101_1011; //2
public const byte Num3 = 0b_0100_1111; //3
public const byte Num4 = 0b_0110_0110; //4
public const byte Num5 = 0b_0110_1101; //5
public const byte Num6 = 0b_0111_1101; //6
public const byte Num7 = 0b_0000_0111; //7
public const byte Num8 = 0b_0111_1111; //8
public const byte Num9 = 0b_0110_1111; //9
public const byte DP = 0b_1000_0000; //小数点
public static byte GetData(char c) =>
c switch
{
'0' => Num0,
'1' => Num1,
'2' => Num2,
'3' => Num3,
'4' => Num4,
'5' => Num5,
'6' => Num6,
'7' => Num7,
'8' => Num8,
'9' => Num9,
_ => Num0
};
}
}
构造函数有三个参数。
public TM1638(int stbPin, int clkPin, int dioPin);
分别代表连接三个引脚的 GPIO 接口号。
比如,老周测试时用的这三个口。
所以,new 的时候就这样写:
TM1638 dev = new(13, 19, 26);
可以用以下程序测试一下。
static void Main(string[] args)
{
using TM1638 dev = new(13, 19, 26);
while (true)
{
int key = dev.ReadKey();
if(key > -1)
{
Console.Write(key + 1);
}
Thread.Sleep(100);
}
}
转自:东邪独孤
链接:cnblogs.com/tcjiaan/p/14951466.html
- EOF -
看完本文有收获?请转发分享给更多人
推荐关注「DotNet」,提升.Net技能
点赞和在看就是最大的支持❤️